﻿

#region Using Directives
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Hoo.Windows.Shell.Util;
using Microsoft.Win32;
using System.IO;
using Hoo.Windows.Shell.Properties;
using Hoo.Windows.Shell.Menu;

#endregion


namespace Hoo.Windows.Shell {

    /// <summary>
    /// A root class for context menu. This class meets the reuqirement of Windows Shell, and friendly to Microsoft.net.
    /// It is help developer to insert new menus easily.<p/>
    /// 	<list type="number">
    /// 		<item>Inherits from this class.</item>
    /// 		<item>Implements abstrct method <code>InsertMenus</code></item>
    /// 	</list>
    /// </summary>
    /// <remarks>
    /// This class does not support dispose and destructor.
    /// </remarks>
    public abstract class ContextMenu : IContextMenu, IShellExtInit {

        protected uint DropFileHandler = 0; // Handler of drop & drap file objects.
        protected int FirstCommandId = 0;   //Beginning of menu ID.

        protected static string Title = "Hoo Path Toy"; //Registry title
        protected const string cAllFileObject = "*";
        protected const string cDirectory = "Directory";
        protected const string cDirectoryBackground = @"Directory\Background";
        protected const string cFolder = "Folder";

        protected static IList<String> RegistryFileTypes = new List<String>() { cAllFileObject, cDirectory, cDirectoryBackground };


        protected string CurrentDirectory = Environment.GetEnvironmentVariable("SystemRoot");
        protected IList<String> SelectedFiles;

        protected IList<MenuItem> MenuItems = null;


        /// <summary>
        ///     Implementing the QueryContextMenu of <see cref="IContextMenu"/> interface.
        /// </summary>
        /// <param name="hMenu">The menu to which additional items are added</param>
        /// <param name="iMenu">Specifies the location on the menu (must never be –1).</param>
        /// <param name="idCmdFirst">The beginning of the specified range</param>
        /// <param name="idCmdLast">The end of the specified range. 
        /// The number of items added is also the maximum menu item ID offset (USHORT) in the code field (LOWORD) of the scode.</param>
        /// <param name="uFlags">Specifies the context.</param>
        /// <returns>number of items that were added to the context menu</returns>
        int IContextMenu.QueryContextMenu(uint hMenu, uint iMenu, int idCmdFirst, int idCmdLast, uint uFlags) {


            Logger.Debug(String.Format("Begin QueryContextMenu(), hMenu: {0}, iMenu: {1}, first: {2}, last: {3}, flags {4}", hMenu, iMenu, idCmdFirst, idCmdLast, uFlags));

            FirstCommandId = idCmdFirst;

            int id = idCmdFirst; //(id - idCmdFirst) stands by count of new menu items.

            try {

                if ((uFlags & 0xf) == 0 || (uFlags & (uint)CMF.CMF_EXPLORE) != 0) {
                    if ((uFlags & (uint)CMF.CMF_NODEFAULT) == 0) { // When CMF_NODEFAULT, it triggered by "File" menu
                        GetSelectedFiles();
                        id = InsertMenus(hMenu, iMenu, new HooPathToyMenu());
                    }

                }

                if((uFlags & (uint)CMF.CMF_CANRENAME) !=0)
                    Logger.Debug("Flag shows: CMF_CANRENAME");

                if ((uFlags & (uint)CMF.CMF_NORMAL) != 0)
                    Logger.Debug("Flag shows: CMF_NORMAL");

                if ((uFlags & (uint)CMF.CMF_DEFAULTONLY) != 0)
                    Logger.Debug("Flag shows: CMF_DEFAULTONLY");

                if ((uFlags & (uint)CMF.CMF_VERBSONLY) != 0)
                    Logger.Debug("Flag shows: CMF_VERBSONLY");

                if ((uFlags & (uint)CMF.CMF_NOVERBS) != 0)
                    Logger.Debug("Flag shows: CMF_NOVERBS");

                if ((uFlags & (uint)CMF.CMF_NODEFAULT) != 0)
                    Logger.Debug("Flag shows: CMF_NODEFAULT");

                if ((uFlags & (uint)CMF.CMF_INCLUDESTATIC) != 0)
                    Logger.Debug("Flag shows: CMF_INCLUDESTATIC");

                //if ((uFlags & (uint)CMF.CMF_RESERVED) != 0)
                //    Logger.Info("Flag shows: CMF_RESERVED");

                if ((uFlags & (uint)CMF.CMF_EXPLORE) != 0)
                    Logger.Debug("Flag shows: CMF_EXPLORE");

                Logger.Info(string.Format("Exit QueryContextMenu with menu ID: {0}", id));
            } catch (Exception e) {
                Logger.Error(e);
            }
            return id + 1 - idCmdFirst;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="hMenu"></param>
        /// <param name="iMenu"></param>
        /// <returns></returns>
        /// <remarks>
        /// Make sure initialize <code>MenuItems</code> in this method of concrete class.
        /// </remarks>
        protected abstract int InsertMenus(uint hMenu, uint iMenu, MenuItem mainMenu);

        protected void GetSelectedFiles() {

            if (SelectedFiles == null)
                SelectedFiles = new List<String>();
            else
                SelectedFiles.Clear();

            uint count = Win32Helpers.DragQueryFile(DropFileHandler, 0xffffffff, null, 0);
            //Logger.Info("Count of selected File: " + nselected);
            if (count == 0xffffffff) {
                //No file selected.                    
            } else if (count >= 1) {
                StringBuilder sb = new StringBuilder(1024);
                Win32Helpers.DragQueryFile(DropFileHandler, 0, sb, sb.Capacity + 1);
                string path = sb.ToString();
                Logger.Debug("Get current path: " + path);
                SelectedFiles.Add(path);

                //Set current directory base on first selection
                //If the first selection is a file, set its parent directory as current directory.
                //If the first selection is a folder, set it as current directory.
                if (Directory.Exists(path)) {
                    Logger.Debug("Current path is directory! ");
                    CurrentDirectory = path;
                } else {
                    CurrentDirectory = path.Substring(0, path.LastIndexOf('\\'));
                    Logger.Debug("Current path is not a directory, set its parent as current directory! " + CurrentDirectory);

                }

                for (uint i = 1; i < count; i++) {
                    StringBuilder sb2 = new StringBuilder(1024);
                    Win32Helpers.DragQueryFile(DropFileHandler, i, sb2, sb2.Capacity + 1);
                    SelectedFiles.Add(sb2.ToString());
                    //Logger.Info("Selected Files: " + sb.ToString());
                }
            }
        }


        /// <summary>
        ///     Display command text in explorer status bar
        /// </summary>
        /// <param name="idCmd"></param>
        /// <param name="uFlags"></param>
        /// <param name="pwReserved"></param>
        /// <param name="commandString"></param>
        /// <param name="cchMax"></param>
        void IContextMenu.GetCommandString(int idCmd, uint uFlags, int pwReserved, StringBuilder commandString, int cchMax) {
            String msg = String.Format("GetCommandString: idCmd {0}, uFlags {1}, pwReserved {2}, commandString {3}, cchMax {4}", idCmd, uFlags, pwReserved, commandString.ToString(), cchMax);
            Logger.Debug(msg);

            switch (uFlags) {
                case (uint)GCS.VERB:
                    Logger.Debug("Hoo Context Menu Verb");
                    commandString = new StringBuilder("Hoo Context Menu Verb");
                    break;
                case (uint)GCS.VERBW:
                    Logger.Debug("Hoo Context Menu VerbW");
                    commandString = new StringBuilder("Hoo Context Menu VerbW");
                    break;
                case (uint)GCS.HELPTEXT:
                    Logger.Debug("Hoo Context Menu Help");
                    commandString = new StringBuilder("Hoo Context Menu Help");
                    break;
                case (uint)GCS.HELPTEXTW:
                    Logger.Debug("Hoo Context Menu Help W");
                    commandString = new StringBuilder("Hoo Context Menu Help W");
                    break;
                case (uint)GCS.VALIDATE:
                    Logger.Debug("VALIDATE");
                    break;
                case (uint)GCS.VALIDATEW:
                    Logger.Debug("VALIDATEW");
                    break;
            }

            msg = String.Format("End GetCommandString: commandString {0}", commandString.ToString());
            Logger.Debug(msg);

        }

        /// <summary>
        ///     Performs the command operations here
        /// </summary>
        /// <param name="pici"></param>
        uint IContextMenu.InvokeCommand(IntPtr pici) {
            Logger.Debug("Begin InvokeCommand() " + pici.ToString());
            try {
                Type commandType = typeof(INVOKECOMMANDINFO);
                INVOKECOMMANDINFO ici = (INVOKECOMMANDINFO)Marshal.PtrToStructure(pici, commandType);

                String msg = String.Format("Detail of ici: directory {0}, parameters {1}, verb {2}, wnd {3}, mask {4}", ici.directory, ici.parameters, ici.verb, ici.wnd, ici.fMask);
                Logger.Debug(msg);

                int selectedMenu = ici.verb + FirstCommandId;
                foreach (MenuItem item in MenuItems) {
                    if (selectedMenu == item.ID) {
                        item.Execute(CurrentDirectory, SelectedFiles);
                        break;
                    }
                }

            } catch (Exception ex) {
                Logger.Error(ex);
                return (uint)ErrorCode.INVALIDARG;
            }
            Logger.Debug("End InvokeCommand() ");
            return (uint)ErrorCode.S_OK;
        }

              

        /// <summary>
        ///     Implementing the initialize method
        /// </summary>
        /// <param name="pidlFolder"></param>
        /// <param name="lpdobj"></param>
        /// <param name="hKeyProgID"></param>
        /// <returns></returns>
        int IShellExtInit.Initialize(IntPtr pidlFolder, IntPtr lpdobj, uint hKeyProgID) {
            try {
                Logger.BlankLine();
                Logger.Debug("Begin IShellExtInit.Initialize ");
                StringBuilder folderPath = new StringBuilder(1024);
                if (pidlFolder != IntPtr.Zero) {
                    if (Win32Helpers.SHGetPathFromIDListW(pidlFolder, folderPath)) {
                        CurrentDirectory = folderPath.ToString();
                        Logger.Debug("Set current folder from pidlFolder: " + CurrentDirectory);
                    } else {
                        Logger.Warn("Could not convert PIDL " + pidlFolder);
                    }
                }


                if (lpdobj != (IntPtr)0) {
                    // Get info about the directory
                    Hoo.Windows.Shell.Util.IDataObject dataObject = (Hoo.Windows.Shell.Util.IDataObject)Marshal.GetObjectForIUnknown(lpdobj);
                    FORMATETC fmt = new FORMATETC();
                    fmt.cfFormat = CLIPFORMAT.CF_HDROP;
                    fmt.ptd = 0;
                    fmt.dwAspect = DVASPECT.DVASPECT_CONTENT;
                    fmt.lindex = -1;
                    fmt.tymed = TYMED.TYMED_HGLOBAL;
                    STGMEDIUM medium = new STGMEDIUM();
                    dataObject.GetData(ref fmt, ref medium);
                    DropFileHandler = medium.hGlobal;
                }
            } catch (Exception) {
            }
            Logger.Debug("End IShellExtInit.Initialize ");
            return 0;
        }

        

        /// <summary>
        ///     Methods that will be invoked during the RCW generation
        /// </summary>
        /// <param name="str1"></param>
        [ComRegisterFunction()]
        static void RegisterServer(Type t) {
            try {
                string clsId = t.GUID.ToString("B");
                Logger.Info("Begin register assembly: " + t.FullName + ", " + clsId);

                String title = Title;
                if (title == null) throw new Exception("The registry title of " + t.FullName + " is missed! Please initialize Title in a static constructor.");

                IList<string> fileTypes = RegistryFileTypes;
                if (fileTypes == null | fileTypes.Count <= 0) throw new Exception("The list of to be registered file types is missed! Please initialize RegistryFileTypes in a static constructor.");

                // For Winnt set it as an approved shellex     
                RegistryKey root = Registry.LocalMachine;
                RegistryKey rk = root.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved", true);
                rk.SetValue(clsId, title);
                rk.Close();

                root = Registry.ClassesRoot;
                foreach (String fileType in fileTypes) {
                    string keyName = fileType + @"\shellex\ContextMenuHandlers\" + Title;
                    rk = root.OpenSubKey(keyName, true);
                    if (rk == null)
                        rk = root.CreateSubKey(keyName);
                    rk.SetValue("", clsId);
                    rk.Close();
                }
                               

                System.Reflection.Assembly a = System.Reflection.Assembly.GetExecutingAssembly();
                Logger.Info("Assembly full name: " + a.FullName);
                Logger.Info("Assembly Location: " + a.Location);
                Logger.Info("Assembly code base: " + a.CodeBase);
                Logger.Debug("Assembly escaped code base" + a.EscapedCodeBase);
                Logger.Info("AppDomain: " + AppDomain.CurrentDomain.BaseDirectory + ", " + AppDomain.CurrentDomain.FriendlyName);
                

            } catch (Exception e) {
                Logger.Error(e);
                throw e;
            }
        }



        /// <summary>
        ///     Methods that will be invoked during the RCW generation
        /// </summary>
        /// <param name="str1"></param>
        [ComUnregisterFunction()]        
        static void UnregisterServer(Type t) {
            try {
                string clsId = t.GUID.ToString("B");
                Logger.Info("Begin unregister assembly: " + t.FullName + ", " + clsId);

                String title = Title;
                if (title == null) throw new Exception("The registry title of " + t.FullName + " is missed! Please initialize Title in a static constructor.");

                IList<string> fileTypes = RegistryFileTypes;
                if (fileTypes == null | fileTypes.Count <= 0) throw new Exception("The list of to be registered file types is missed! Please initialize RegistryFileTypes in a static constructor.");

                // Remove ShellExtenstions registration
                RegistryKey root = Registry.LocalMachine;
                RegistryKey rk = root.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved", true);
                rk.DeleteValue(clsId);
                rk.Close();

                // Delete  regkey
                root = Registry.ClassesRoot;
                foreach (String fileType in fileTypes) {
                    string keyName = fileType + @"\shellex\ContextMenuHandlers\" + title;
                    root.DeleteSubKey(keyName, false);
                }
                
            } catch (Exception e) {
                Logger.Error(e);
            }
        }
    }
}
